import java.util.*;
import java.io.*;

public class Example {
    static final String bookPath = "Address Book.dat";
    static final String tmpFilePath = "$Address Book.dat"; // 添加新联系人的信息时，为保证安全性，先向临时文件写入信息，再移动文件
    static TreeMap<Integer, String> map; // id-姓名映射。id从1开始
    static final int recordLen = 94;

    // 支持的功能：查询、增加、更新和帮助
    // 文件存储格式：记录数 + 序号-姓名映射表 + 记录
    // 记录格式：姓名4字符（8字节），电话11字符（22字节），地址32字符（64字节），
    // 姓名、电话和地址长度不足时在右侧补零
    // 一条记录长度47字节（94字符）

    // 异常处理设计思路：
    // 在上层方法显示异常信息，确保一个异常只显示一次异常信息
    // 只有在发生导致程序无法继续工作的异常时才终止程序
    public static void main(String args[]) {
        Scanner in = new Scanner(System.in);
        // 初始化id-姓名映射表
        try {
            map = initialize();
        }
        catch (IOException ex) {
            printErrorMsg(ex);
            return;
        }
        // 假如数据文件不存在，则予以创建
        // 只会用"rw"模式下的RandomAccessFile打开临时文件，因而无需预先创建临时文件
        try {
            File dataFile = new File(bookPath);
            if (!dataFile.exists())
                dataFile.createNewFile();
        }
        catch (IOException ex) {
            printErrorMsg(ex);
            return;
        }

        System.out.println("（输入半角\"?\"获得帮助）");
        while (true) {
            System.out.print(">");
            String cmd = in.nextLine();
            String[] arguments = cmd.split(" ");
            switch (arguments[0]) {
                case "?":
                    help();
                    break;
                case "query":
                    if (arguments.length < 2) {
                        System.out.println("无法解析命令 " + cmd + "。");
                        break;
                    }
                    query(arguments[1]);
                    break;
                case "add":
                    add(in);
                    break;
                case "update":
                    if (arguments.length < 2) {
                        System.out.println("无法解析命令 " + cmd + "。");
                        break;
                    }
                    update(arguments[1], in);
                    break;
                case "quit":
                    return;
                case "":
                    // 直接敲击回车
                    break;
                default:
                    System.out.println("无法解析命令 " + cmd + "。");
                    break;
            }
        }
    }

    private static TreeMap<Integer, String> initialize() throws IOException {
        File file = new File(bookPath);
        TreeMap<Integer, String> map = new TreeMap<>();
        if (!file.exists())
            return map;
        try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(file), 8192));) {
            int numOfRecords = in.readInt();
            // 读取映射表
            for (int i = 1; i <= numOfRecords; i++) {
                int id = in.readInt();
                String name = readName(in);
                map.put(id, name);
            }
            return map;
        }
        catch (IOException ex) {
            throw new IOException("在读取id-姓名映射表时发生IO异常。");
        }
    }

    private static void help() {
        System.out.println("query all");
        System.out.println("query [序号]");
        System.out.println("query [姓名]");
        System.out.println("add");
        System.out.println("update [序号]");
        System.out.println("update [姓名]");
        System.out.println("quit");
        System.out.println("注：");
        System.out.println("1. 中括号在提示信息中起强调作用，无需输入。");
        System.out.println("2. 请不要输入多余的空格。");
    }

    // 允许显示异常信息
    private static void query(String arg) {
        // 查询全部
        if (arg.equals("all")) {
            if (map.size() == 0) {
                System.out.println("尚不存在任何记录。");
                return;
            }
            try (DataInputStream in = new DataInputStream(
                    new BufferedInputStream(new FileInputStream(bookPath), 8192))) {
                // 移动至第一条信息的位置
                in.skipBytes(4 + map.size() * (4 + 8));
                for (int i = 1; i <= map.size(); i++) {
                    String name = readName(in);
                    String phoneNumber = readPhoneNumber(in);
                    String address = readAddress(in);
                    printInfo(i, new Info(name, phoneNumber, address));
                }
            }
            catch (IOException ex) {
                printExceptionMsg("查询所有人员信息", ex);
                return;
            }
        }
        // 查询序号
        else if (isInt(arg)) {
            int id = Integer.parseInt(arg);
            if (id < 1 || id > map.size()) {
                System.out.println("序号越界，请检查您的输入。");
                return;
            }
            try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r");) {
                readAndPrintInfo(id, raf);
            }
            catch (IOException ex) {
                printExceptionMsg("读取序号为" + id + "的信息", ex);
                return;
            }
        }
        // 查询人名
        else {
            // 存储的姓名长度统一为4，需要补全参数，使得String.equals能正确发挥作用
            if (arg.length() < 4)
                arg = complete(arg, 4);
            // 检查是否存在名为arg的联系人
            boolean contains = false;
            for (String name : map.values()) {
                if (name.equals(arg)) {
                    contains = true;
                    break;
                }
            }
            if (!contains) {
                System.out.println("不存在名为 " + strip(arg) + " 的联系人。");
                return;
            }
            try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r");) {
                for (Map.Entry<Integer, String> entry : map.entrySet()) {
                    if (entry.getValue().equals(arg)) {
                        readAndPrintInfo(entry.getKey(), raf);
                    }
                }
            }
            catch (IOException ex) {
                printExceptionMsg("读取" + arg + "的信息", ex);
                return;
            }
        }
    }

    // 允许显示异常信息
    private static void add(Scanner in) {
        System.out.println("请输入联系人的姓名、电话号码和地址。");
        System.out.println("注意：");
        System.out.println("1. 姓名、电话号码和地址中不应包含空格。不允许输入\"all\"作为姓名。");
        System.out.println("2. 姓名的最大长度为4，电话号码的最大长度为11，地址的最大长度为32，超出的部分将被忽略。");
        // 读取用户输入
        Info info = new Info();
        if (!readNewInfo(in, info))
            return;
        // 更新内存中的id-姓名映射，注意不要重复更新
        map.put(map.size() + 1, info.name);
        // 更新文件
        try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r")) {
            TreeMap<Integer, Info> infoMap = readAllInfo(raf);
            infoMap.put(map.size(), info);
            writeAllInfo(infoMap, tmpFilePath);
        }
        catch (IOException ex) {
            printExceptionMsg("写入新联系人的信息", ex);
            // 撤销更改
            map.remove(map.size());
            File tmpFile = new File(tmpFilePath);
            tmpFile.delete();
            return;
        }
        // 移动文件
        File tmpFile = new File(tmpFilePath);
        File srcFile = new File(bookPath);
        srcFile.delete();
        tmpFile.renameTo(srcFile);
        System.out.println("完成");
    }

    // 允许显示异常信息
    private static void update(String arg, Scanner in) {
        // 依据序号更新
        if (isInt(arg)) {
            int id = Integer.parseInt(arg);
            if (id < 1 || id > map.size()) {
                System.out.println("序号越界，请检查您的输入。");
                return;
            }
            String originName = map.get(id);
            // 接收新信息
            Info info = new Info();
            if (!readNewInfo(in, info))
                return;
            // 更新id-姓名映射
            map.put(id, info.name);
            // 更新文件
            try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r")) {
                TreeMap<Integer, Info> infoMap = readAllInfo(raf);
                infoMap.put(id, info);
                writeAllInfo(infoMap, tmpFilePath);
            }
            catch (IOException ex) {
                printExceptionMsg("更改序号为" + id + "的信息", ex);
                // 撤销更改
                map.put(id, originName);
                File tmpFile = new File(tmpFilePath);
                tmpFile.delete();
                return;
            }
        }
        // 依据姓名更新
        else {
            // 检查是否存在此联系人，以及是否存在同名联系人
            int count = 0;
            ArrayList<Integer> ids = new ArrayList<>();
            if (arg.length() < 4)
                arg = complete(arg, 4);
            for (Map.Entry<Integer, String> entry : map.entrySet()) {
                if (entry.getValue().equals(arg)) {
                    count++;
                    ids.add(entry.getKey());
                }
            }
            // 不存在此联系人
            if (count == 0) {
                System.out.println("不存在名为 " + strip(arg) + " 的联系人，请检查您的输入。");
                return;
            }
            // 存在同名联系人
            else if (count > 1) {
                // 输出同名联系人信息
                System.out.println("存在不止一个名为 " + strip(arg) + " 的联系人：");
                try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r");) {
                    for (int id : ids)
                        readAndPrintInfo(id, raf);
                }
                catch (IOException ex) {
                    printExceptionMsg("检索同名联系人信息", ex);
                    return;
                }
                System.out.print("请输入要修改的记录的序号：");
                // 接受联系人名
                String input = in.nextLine();
                // 检查输入合法性
                if (!isInt(input)) {
                    System.out.println("无法解析输入信息 " + input + "。");
                    return;
                }
                int id = Integer.parseInt(input);
                if (id < 1 || !ids.contains(id)) {
                    System.out.println("序号越界，请检查您的输入。");
                    return;
                }
                // 接受更新的联系人信息
                Info info = new Info();
                info.name = map.get(id);
                readNewInfo(in, info);
                // 更新文件
                try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r")) {
                    TreeMap<Integer, Info> infoMap = readAllInfo(raf);
                    infoMap.put(id, info);
                    writeAllInfo(infoMap, tmpFilePath);
                }
                catch (IOException ex) {
                    printExceptionMsg("更改序号为" + id + "的信息", ex);
                    // 撤销更改
                    File tmpFile = new File(tmpFilePath);
                    tmpFile.delete();
                    return;
                }
            }
            // 存在唯一符合输入的联系人
            else {
                int id = ids.get(0);
                // 接受更新的联系人信息
                Info info = new Info();
                info.name = map.get(id);
                readNewInfo(in, info);
                // 更新文件
                try (RandomAccessFile raf = new RandomAccessFile(bookPath, "r")) {
                    TreeMap<Integer, Info> infoMap = readAllInfo(raf);
                    infoMap.put(id, info);
                    writeAllInfo(infoMap, tmpFilePath);
                }
                catch (IOException ex) {
                    printExceptionMsg("更改序号为" + id + "的信息", ex);
                    // 撤销更改
                    File tmpFile = new File(tmpFilePath);
                    tmpFile.delete();
                    return;
                }
            }
        }
        // 移动文件
        File tmpFile = new File(tmpFilePath);
        File srcFile = new File(bookPath);
        srcFile.delete();
        tmpFile.renameTo(srcFile);
        System.out.println("完成");
    }

    private static boolean isInt(String str) {
        if (str == null)
            return false;
        int start = 0;
        if (str.charAt(0) == '-') {
            if (str.length() < 2)
                return false;
            start = 1;
        }
        for (int i = start; i < str.length(); i++) {
            char ch = str.charAt(i);
            if (!(ch >= '0' && ch <= '9'))
                return false;
        }
        return true;
    }

    private static String readName(DataInputStream in) throws IOException {
        char[] arr = new char[4];
        for (int i = 1; i <= 4; i++) {
            arr[i - 1] = in.readChar();
        }
        return new String(arr);
    }

    private static String readName(RandomAccessFile raf) throws IOException {
        char[] arr = new char[4];
        for (int i = 1; i <= 4; i++) {
            arr[i - 1] = raf.readChar();
        }
        return new String(arr);
    }

    private static String readPhoneNumber(DataInputStream in) throws IOException {
        char[] arr = new char[11];
        for (int i = 1; i <= 11; i++) {
            arr[i - 1] = in.readChar();
        }
        return new String(arr);
    }

    private static String readPhoneNumber(RandomAccessFile raf) throws IOException {
        char[] arr = new char[11];
        for (int i = 1; i <= 11; i++) {
            arr[i - 1] = raf.readChar();
        }
        return new String(arr);
    }

    private static String readAddress(DataInputStream in) throws IOException {
        char[] arr = new char[32];
        for (int i = 1; i <= 32; i++) {
            arr[i - 1] = in.readChar();
        }
        return new String(arr);
    }

    private static String readAddress(RandomAccessFile raf) throws IOException {
        char[] arr = new char[32];
        for (int i = 1; i <= 32; i++) {
            arr[i - 1] = raf.readChar();
        }
        return new String(arr);
    }

    // 确保只有上层方法调用
    private static void printErrorMsg(Exception ex) {
        System.out.println("遭遇错误，程序被迫中止。如果需要寻求支持人员的帮助，请提供以下信息：");
        ex.printStackTrace();
    }

    // 确保只有上层方法调用
    private static void printExceptionMsg(String missonName, Exception ex) {
        System.out.println("在 " + missonName + " 时捕获到异常：");
        ex.printStackTrace();
    }

    private static void readAndPrintInfo(int id, RandomAccessFile raf) throws IOException {
        moveRaf(id, raf);
        String name = readName(raf);
        String phoneNumber = readPhoneNumber(raf);
        String address = readAddress(raf);
        printInfo(id, new Info(name, phoneNumber, address));
    }

    // 包含了新加入的联系人信息
    // 为保证安全性，先向临时文件写入
    private static void writeAllInfo(TreeMap<Integer, Info> infoMap, String tmpFilePath) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(tmpFilePath, "rw")) {
            // 若文件已存在（可能是上次写入失败后留下的），清空文件
            raf.setLength(0);
            raf.writeInt(infoMap.size());
            // 写入id-姓名映射表
            for (int i = 1; i <= infoMap.size(); i++) {
                raf.writeInt(i);
                raf.writeChars(infoMap.get(i).name);
            }
            for (int i = 1; i <= infoMap.size(); i++) {
                writeInfo(infoMap.get(i), raf);
            }
        }
    }

    private static void writeInfo(Info info, RandomAccessFile raf) throws IOException {
        raf.writeChars(info.name);
        raf.writeChars(info.phoneNumber);
        raf.writeChars(info.address);
    }

    private static Info readInfo(RandomAccessFile raf) throws IOException {
        String name = readName(raf);
        String phoneNumber = readPhoneNumber(raf);
        String address = readAddress(raf);
        return new Info(name, phoneNumber, address);
    }

    private static TreeMap<Integer, Info> readAllInfo(RandomAccessFile raf) throws IOException {
        TreeMap<Integer, Info> infoMap = new TreeMap<>();
        int num;
        raf.seek(0);
        try {
            num = raf.readInt();
        }
        // 文件中尚无内容
        catch (EOFException ex) {
            return infoMap;
        }
        // 移动到第一条记录的位置
        raf.seek(4 + num * (4 + 8));
        for (int i = 1; i <= num; i++) {
            Info info = readInfo(raf);
            infoMap.put(i, info);
        }
        return infoMap;
    }

    private static void printInfo(int id, Info info) {
        String name = info.name;
        String phoneNumber = info.phoneNumber;
        String address = info.address;
        System.out.println("NO." + id);
        System.out.println("姓名：" + name);
        System.out.println("电话号码：" + phoneNumber);
        System.out.println("地址：" + address);
    }

    // 移动raf到第id条记录所在的位置。id从1开始。
    // 不能在添加新记录的过程中调用。
    private static void moveRaf(int id, RandomAccessFile raf) throws IOException {
        raf.seek(4 + map.size() * (4 + 8) + (id - 1) * recordLen);
    }

    static String complete(String src, int length) {
        for (int i = src.length(); i < length; i++) {
            src += " ";
        }
        return src;
    }

    static String formalize(String input, int length) {
        if (input.length() > length)
            return input.substring(0, length);
        else
            return complete(input, length);
    }

    private static boolean readNewInfo(Scanner in, Info info) {
        if (info.name == null) {
            System.out.print("姓名：");
            String name = in.nextLine();
            // 需要阻止名为"all"的人员加入，避免二义性
            if (name.equals("all")) {
                System.out.println("不允许输入\"all\"作为姓名。");
                return false;
            }
            info.name = formalize(name, 4);
        }
        if (info.phoneNumber == null) {
            System.out.print("电话号码：");
            info.phoneNumber = formalize(in.nextLine(), 11);
        }
        if (info.address == null) {
            System.out.print("地址：");
            info.address = formalize(in.nextLine(), 32);
        }
        return true;
    }

    // 去除字符串两端的空白符
    private static String strip(String str) {
        String regex = "\\s";
        StringBuilder builder = new StringBuilder(str);
        while (builder.length() > 0 && builder.substring(0, 1).matches(regex))
            builder.delete(0, 1);
        while (builder.length() > 0 && builder.substring(builder.length() - 1, builder.length()).matches(regex))
            builder.delete(builder.length() - 1, builder.length());
        return new String(builder);
    }
}

class Info {
    String name = null;
    String phoneNumber = null;
    String address = null;

    public Info(String name, String phoneNumber, String address) {
        this.name = Example.formalize(name, 4);
        this.phoneNumber = Example.formalize(phoneNumber, 11);
        this.address = Example.formalize(address, 32);
    }

    public Info() {

    }
}